第 4 章:認識與學習 BASH
透過『 Shell 』將我們輸入的指令與 Kernel 溝通,好讓 Kernel 可以控制硬體來正確無誤的工作
type 查詢指令是否為 Bash shell 的內建命令
type
type [-tpa] name
選項與參數:
:不加任何選項與參數時,type 會顯示出 name 是外部指令還是 bash 內建指令
-t :當加入 -t 參數時,type 會將 name 以底下這些字眼顯示出他的意義:
file :表示為外部指令;
alias :表示該指令為命令別名所設定的名稱;
builtin :表示該指令為 bash 內建的指令功能;
-p :如果 後面接的 name 為外部指令時,才會顯示完整檔名;
-a :會由 PATH 變數定義的路徑中,將所有含 name 的指令都列出來,包含 alias範例一:查詢一下 ls 這個指令是否為 bash 內建?
type ls
ls is aliased to `ls --color=auto' <==未加任何參數,列出 ls 的最主要使用情況
type -t ls
alias <==僅列出 ls 執行時的依據
type -a ls
ls is aliased to `ls --color=auto' <==最先使用 aliase
ls is /usr/bin/ls <==還有找到外部指令在 /bin/ls
範例二:那麼 cd 呢?
type cd
cd is a shell builtin <==看到了嗎? cd 是 shell 內建指令
env 觀察環境變數與常見環境變數說明
env
範例一:列出目前的 shell 環境下的所有環境變數與其內容。
env
HOSTNAME=study.centos.vbird <== 這部主機的主機名稱
TERM=xterm <== 這個終端機使用的環境是什麼類型
SHELL=/bin/bash <== 目前這個環境下,使用的 Shell 是哪一個程式?
HISTSIZE=1000 <== 『記錄指令的筆數』在 CentOS 預設可記錄 1000 筆
OLDPWD=/home/dmtsai <== 上一個工作目錄的所在
LC_ALL=en_US.utf8 <== 由於語系的關係,鳥哥偷偷丟上來的一個設定
USER=dmtsai <== 使用者的名稱啊!
LS_COLORS=rs=0:di=01;34:ln=01;36:mh=00:pi=40;33:so=01;35:do=01;35:bd=40;33;01:cd=40;33;01:
or=40;31;01:mi=01;05;37;41:su=37;41:sg=30;43:ca=30;41:tw=30;42:ow=34;42:st=37;44:ex=01;32:
*.tar=01... <== 一些顏色顯示
MAIL=/var/spool/mail/dmtsai <== 這個使用者所取用的 mailbox 位置
PATH=/usr/local/bin:/usr/bin:/usr/local/sbin:/usr/sbin:/home/dmtsai/.local/bin:/home/dmtsai/bin
PWD=/home/dmtsai <== 目前使用者所在的工作目錄 (利用 pwd 取出!)
LANG=zh_TW.UTF-8 <== 這個與語系有關,底下會再介紹!
HOME=/home/dmtsai <== 這個使用者的家目錄啊!
LOGNAME=dmtsai <== 登入者用來登入的帳號名稱
_=/usr/bin/env <== 上一次使用的指令的最後一個參數(或指令本身)
set 觀察所有變數 (含環境變數與自訂變數)
bash 可不只有環境變數,還與 bash 操作介面有關的變數,以及使用者自己定義的變數存在的。
set
set
BASH=/bin/bash <== bash 的主程式放置路徑
BASH_VERSINFO=([0]="4" [1]="2" [2]="46" [3]="1" [4]="release" [5]="x86_64-redhat-linux-gnu")
BASH_VERSION='4.2.46(1)-release' <== 這兩行是 bash 的版本啊!
COLUMNS=90 <== 在目前的終端機環境下,使用的欄位有幾個字元長度
HISTFILE=/home/dmtsai/.bash_history <== 歷史命令記錄的放置檔案,隱藏檔
HISTFILESIZE=1000 <== 存起來(與上個變數有關)的檔案之指令的最大紀錄筆數。
HISTSIZE=1000 <== 目前環境下,記憶體中記錄的歷史命令最大筆數。
IFS=$' \\t\\n' <== 預設的分隔符號
LINES=20 <== 目前的終端機下的最大行數
MACHTYPE=x86_64-redhat-linux-gnu <== 安裝的機器類型
OSTYPE=linux-gnu <== 作業系統的類型!
PS1='[\\u@\\h \\W]\\$ ' <== PS1 就厲害了。這個是命令提示字元,也就是我們常見的
[root@www ~]# 或 [dmtsai ~]$ 的設定值啦!可以更動的!
PS2='> ' <== 如果你使用跳脫符號 (\\) 第二行以後的提示字元也
$ <== 目前這個 shell 所使用的 PID
? <== 剛剛執行完指令的回傳值。
...
export 自訂變數轉成環境變數
export
export
declare -x HISTSIZE="1000"
declare -x HOME="/home/dmtsai"
declare -x HOSTNAME="study.centos.vbird"
declare -x LANG="zh_TW.UTF-8"
declare -x LC_ALL="en_US.utf8"
declare & typeset
declare 或 typeset 是一樣的功能,就是在『宣告變數的類型』。如果使用 declare 後面並沒有接任何參數,那麼 bash 就會主動的將所有的變數名稱與內容通通叫出來,就好像使用 set 一樣。
declare & typeset
declare [-aixr] variable
選項與參數:
-a :將後面名為 variable 的變數定義成為陣列 (array) 類型
-i :將後面名為 variable 的變數定義成為整數數字 (integer) 類型
-x :用法與 export 一樣,就是將後面的 variable 變成環境變數;
-r :將變數設定成為 readonly 類型,該變數不可被更改內容,也不能 unset範例一:讓變數 sum 進行 100+300+50 的加總結果
sum=100+300+50
echo ${sum}
100+300+50 <==咦!怎麼沒有幫我計算加總?因為這是文字型態的變數屬性啊!
declare -i sum=100+300+50
echo ${sum}
450
history 歷史命令
history
alias h='history'
history [n]
history [-c]
history [-raw] histfiles
選項與參數:
n :數字,意思是『要列出最近的 n 筆命令列表』的意思!
-c :將目前的 shell 中的所有 history 內容全部消除
-a :將目前新增的 history 指令新增入 histfiles 中,若沒有加 histfiles ,
則預設寫入 ~/.bash_history
-r :將 histfiles 的內容讀到目前這個 shell 的 history 記憶中;
-w :將目前的 history 記憶內容寫入 histfiles 中!範例一:列出目前記憶體內的所有 history 記憶
history
1017 man bash
1018 ll
1019 history
1020 history
# 列出的資訊當中,共分兩欄,第一欄為該指令在這個 shell 當中的代碼 ,
# 另一個則是指令本身的內容喔!至於會秀出幾筆指令記錄,則與 HISTSIZE 有關!範例二:列出目前最近的 3 筆資料
history 3
1019 history
1020 history
1021 history 3
範例三:立刻將目前的資料寫入 histfile 當中
history -w# 在預設的情況下,會將歷史紀錄寫入 ~/.bash_history 當中!
echo ${HISTSIZE}
1000
萬用字元與特殊符號
萬用字元與特殊符號
符號 | 意義 |
---|---|
* | 代表『 0 個到無窮多個』任意字元 |
? | 代表『一定有一個』任意字元 |
[ ] | 同樣代表『一定有一個在括號內』的字元(非任意字元)。例如 [abcd] 代表『一定有一個字元, 可能是 a, b, c, d 這四個任何一個』 |
[ - ] | 若有減號在中括號內時,代表『在編碼順序內的所有字元』。例如 [0-9] 代表 0 到 9 之間的所有數字,因為數字的語系編碼是連續的! |
[^ ] | 若中括號內的第一個字元為指數符號 (^) ,那表示『反向選擇』,例如 [^abc] 代表 一定有一個字元,只要是非 a, b, c 的其他字元就接受的意思。 |
LANG=C <==由於與編碼有關,先設定語系一下
範例一:找出 /etc/ 底下以 cron 為開頭的檔名
ll -d /etc/cron* <==加上 -d 是為了僅顯示目錄而已
範例二:找出 /etc/ 底下檔名『剛好是五個字母』的檔名
ll -d /etc/????? <==由於 ? 一定有一個,所以五個 ? 就對了
範例三:找出 /etc/ 底下檔名含有數字的檔名
ll -d /etc/*[0-9]* <==記得中括號左右兩邊均需 *
範例四:找出 /etc/ 底下,檔名開頭非為小寫字母的檔名:
ll -d /etc/[^a-z]* <==注意中括號左邊沒有 *
範例五:將範例四找到的檔案複製到 /tmp/upper 中
mkdir /tmp/upper; cp -a /etc/[^a-z]* /tmp/upper
符號 | 內容 |
---|---|
# | 註解符號:這個最常被使用在 script 當中,視為說明!在後的資料均不執行 |
\ | 跳脫符號:將『特殊字元或萬用字元』還原成一般字元 |
; | 連續指令下達分隔符號:連續性命令的界定 (注意!與管線命令並不相同) |
~ | 使用者的家目錄 |
$ | 取用變數前置字元:亦即是變數之前需要加的變數取代值 |
& | 工作控制 (job control):將指令變成背景下工作 |
! | 邏輯運算意義上的『非』 not 的意思! |
/ | 目錄符號:路徑分隔的符號 |
>, >> | 資料流重導向:輸出導向,分別是『取代』與『累加』 |
<, << | 資料流重導向:輸入導向 (這兩個留待下節介紹) |
' ' | 單引號,不具有變數置換的功能 ($ 變為純文字) |
" " | 具有變數置換的功能! ($ 可保留相關功能) |
兩個『 ` 』中間為可以先執行的指令,亦可使用 $( ) | |
( ) | 在中間為子 shell 的起始與結束 |
在中間為命令區塊的組合! |
資料流重導向
就是將某個指令執行後應該要出現在螢幕上的資料, 給他傳輸到其他的地方,例如檔案或者是裝置
什麼是資料流重導向
我們執行一個指令的時候,這個指令可能會由檔案讀入資料,經過處理之後,再將資料輸出到螢幕上。
standard output 與 standard error output
簡單的說,標準輸出指的是『指令執行所回傳的正確的訊息』,而標準錯誤輸出可理解為『 指令執行失敗後,所回傳的錯誤訊息』。舉個簡單例子來說,我們的系統預設有 /etc/crontab 但卻無 /etc/vbirdsay, 此時若下達『 cat /etc/crontab /etc/vbirdsay 』這個指令時,cat 會進行:
- 標準輸出:讀取 /etc/crontab 後,將該檔案內容顯示到螢幕上;
- 標準錯誤輸出:因為無法找到 /etc/vbirdsay,因此在螢幕上顯示錯誤訊息
不管正確或錯誤的資料都是預設輸出到螢幕上,所以螢幕當然是亂亂的!那能不能透過某些機制將這兩股資料分開呢? 當然可以啊!那就是資料流重導向的功能啊!資料流重導向可以將 standard output (簡稱 stdout) 與 standard error output (簡稱 stderr) 分別傳送到其他的檔案或裝置去,而分別傳送所用的特殊字元則如下所示:
- 標準輸入 (stdin) :代碼為 0 ,使用
<
或<<
; - 標準輸出 (stdout):代碼為 1 ,使用
>
或>>
; - 標準錯誤輸出(stderr):代碼為 2 ,使用
2>
或2>>
;
standard output 與 standard error output
範例一:觀察你的系統根目錄 (/) 下各目錄的檔名、權限與屬性,並記錄下來
ll / <==此時螢幕會顯示出檔名資訊
ll / > ~/rootfile <==螢幕並無任何資訊
ll ~/rootfile <==有個新檔被建立了!
-rw-rw-r--. 1 dmtsai dmtsai 1078 Jul 9 18:51 /home/dmtsai/rootfile
該檔案 (本例中是 ~/rootfile) 若不存在,系統會自動的將他建立起來,但是當這個檔案存在的時候,那麼系統就會先將這個檔案內容清空,然後再將資料寫入!也就是若以 > 輸出到一個已存在的檔案中,那個檔案就會被覆蓋掉囉!
那如果我想要將資料累加而不想要將舊的資料刪除,那該如何是好?利用兩個大於的符號 (>>) 就好啦!以上面的範例來說,你應該要改成『 ll / >> ~/rootfile 』即可。 如此一來,當 (1) ~/rootfile 不存在時系統會主動建立這個檔案;(2)若該檔案已存在, 則資料會在該檔案的最下方累加進去!
上面談到的是 standard output 的正確資料,那如果是 standard error output 的錯誤資料呢?那就透過 2> 及 2>> 囉!同樣是覆蓋 (2>) 與累加 (2>>) 的特性!我們在剛剛才談到 stdout 代碼是 1 而 stderr 代碼是 2 , 所以這個 2> 是很容易理解的,而如果僅存在 > 時,則代表預設的代碼 1 囉!也就是說:
- 1> :以覆蓋的方法將『正確的資料』輸出到指定的檔案或裝置上;
- 1>>:以累加的方法將『正確的資料』輸出到指定的檔案或裝置上;
- 2> :以覆蓋的方法將『錯誤的資料』輸出到指定的檔案或裝置上;
- 2>>:以累加的方法將『錯誤的資料』輸出到指定的檔案或裝置上;
standard output 與 standard error output
範例二:利用一般身份帳號搜尋 /home 底下是否有名為 .bashrc 的檔案存在
find /home -name .bashrc <==身份是 dmtsai 喔!
find: '/home/arod': Permission denied <== Standard error output
find: '/home/alex': Permission denied <== Standard error output
/home/dmtsai/.bashrc <== Standard output
範例三:承範例二,將 stdout 與 stderr 分存到不同的檔案去
find /home -name .bashrc > list_right 2> list_error
/dev/null 垃圾桶黑洞裝置與特殊寫法
/dev/null
範例四:承範例三,將錯誤的資料丟棄,螢幕上顯示正確的資料
find /home -name .bashrc 2> /dev/null
/home/dmtsai/.bashrc <==只有 stdout 會顯示到螢幕上, stderr 被丟棄了
再想像一下,如果我要將正確與錯誤資料通通寫入同一個檔案去呢?這個時候就得要使用特殊的寫法了! 我們同樣用底下的案例來說明:
/dev/null
範例五:將指令的資料全部寫入名為 list 的檔案中
find /home -name .bashrc > list 2> list <==錯誤
find /home -name .bashrc > list 2>&1 <==正確
find /home -name .bashrc &> list <==正確
上述表格第一行錯誤的原因是,由於兩股資料同時寫入一個檔案,又沒有使用特殊的語法, 此時兩股資料可能會交叉寫入該檔案內,造成次序的錯亂。所以雖然最終 list 檔案還是會產生,但是裡面的資料排列就會怪怪的,而不是原本螢幕上的輸出排序。 至於寫入同一個檔案的特殊語法如上表所示,你可以使用 2>&1 也可以使用 &> ! 一般來說,鳥哥比較習慣使用 2>&1 的語法啦!
standard input <
與 <<
瞭解了 stderr 與 stdout 後,那麼那個 < 又是什麼呀?呵呵!以最簡單的說法來說, 那就是『將原本需要由鍵盤輸入的資料,改由檔案內容來取代』的意思。 我們先由底下的 cat 指令操作來瞭解一下什麼叫做『鍵盤輸入』吧!
standard input <
與 <<
範例六:利用 cat 指令來建立一個檔案的簡單流程
cat > catfiletesting
cat file test<==這裡按下 [ctrl]+d 來離開
cat catfile
testing
cat file test
由於加入 > 在 cat,所以那個 catfile 會被主動的建立,而內容就是剛剛鍵盤上面輸入的那兩行資料
standard input <
與 <<
範例七:用 stdin 取代鍵盤的輸入以建立新檔案的簡單流程
cat > catfile < ~/.bashrc
ll catfile ~/.bashrc
-rw-r--r--. 1 dmtsai dmtsai 231 Mar 6 06:06 /home/dmtsai/.bashrc
-rw-rw-r--. 1 dmtsai dmtsai 231 Jul 9 18:58 catfile
# 注意看,這兩個檔案的大小會一模一樣!幾乎像是使用 cp 來複製一般!
這東西非常的有幫助!尤其是用在類似 mail 這種指令的使用上。 理解 <
之後,再來則是怪可怕一把的 <<
這個連續兩個小於的符號了。 他代表的是『結束的輸入字元』的意思!舉例來講:『我要用 cat 直接將輸入的訊息輸出到 catfile 中, 且當由鍵盤輸入 eof 時,該次輸入就結束』,那我可以這樣做:
standard input <
與 <<
cat > catfile << "eof"
>This is a test.
>OK now stop
>eof <==輸入這關鍵字,立刻就結束而不需要輸入 [ctrl]+d
cat catfile
This is a test.
OK now stop <==只有這兩行,不會存在關鍵字那一行!
看到了嗎?利用 <<
右側的控制字元,我們可以終止一次輸入, 而不必輸入 [ctrl]+d 來結束哩!這對程式寫作很有幫助喔!好了,那麼為何要使用命令輸出重導向呢?我們來說一說吧!
- 螢幕輸出的資訊很重要,而且我們需要將他存下來的時候;
- 背景執行中的程式,不希望他干擾螢幕正常的輸出結果時;
- 一些系統的例行命令 (例如寫在 /etc/crontab 中的檔案) 的執行結果,希望他可以存下來時;
- 一些執行命令的可能已知錯誤訊息時,想以『
2> /dev/null
』將他丟掉時; - 錯誤訊息與正確訊息需要分別輸出時。
- 問:假設我要將 echo "error message" 以 standard error output 的格式來輸出,該如何處置?
- 答:既然有
2>&1
來將2>
轉到1>
去,那麼應該也會有1>&2
吧?沒錯!就是這個概念!因此你可以這樣作:
echo "error message" 1>&2 echo "error message" 2> /dev/null 1>&2
你會發現第一條有訊息輸出到螢幕上,第二條則沒有訊息!這表示該訊息已經是透過 2> /dev/null 丟到垃圾桶去
命令執行的判斷依據: ; , &&, ||
在某些情況下,很多指令我想要一次輸入去執行,而不想要分次執行時,該如何是好?基本上你有兩個選擇, 一個是透過第十二章要介紹的 shell script 撰寫腳本去執行,一種則是透過底下的介紹來一次輸入多重指令喔!
cmd ; cmd (不考慮指令相關性的連續指令下達)
在某些時候,我們希望可以一次執行多個指令,例如在關機的時候我希望可以先執行兩次 sync 同步化寫入磁碟後才 shutdown 電腦,那麼可以怎麼作呢?這樣做呀:
sync; sync; shutdown -h now
在指令與指令中間利用分號 (;) 來隔開,這樣一來,分號前的指令執行完後就會立刻接著執行後面的指令了。 這真是方便啊~再來,換個角度來想,萬一我想要在某個目錄底下建立一個檔案,也就是說,如果該目錄存在的話, 那我才建立這個檔案,如果不存在,那就算了。也就是說這兩個指令彼此之間是有相關性的, 前一個指令是否成功的執行與後一個指令是否要執行有關!那就得動用到 && 或 || 囉!
$? 指令回傳值與 && 或 ||
指令下達情況 | 說明 |
---|---|
cmd1 && cmd2 | 1. 若 cmd1 執行完畢且正確執行($?=0),則開始執行 cmd2。2. 若 cmd1 執行完畢且為錯誤 ($?≠0),則 cmd2 不執行 |
cmd1 |
上述的 cmd1 及 cmd2 都是指令。好了,回到我們剛剛假想的情況,就是想要: (1)先判斷一個目錄是否存在; (2)若存在才在該目錄底下建立一個檔案。由於我們尚未介紹如何判斷式 (test) 的使用,在這裡我們使用 ls 以及回傳值來判斷目錄是否存在啦!
指令回傳值
範例一:使用 ls 查閱目錄 /tmp/abc 是否存在,若存在則用 touch 建立 /tmp/abc/hehe
ls /tmp/abc && touch /tmp/abc/hehe
ls: cannot access /tmp/abc: No such file or directory
# ls 很乾脆的說明找不到該目錄,但並沒有 touch 的錯誤,表示 touch 並沒有執行
mkdir /tmp/abc
ls /tmp/abc && touch /tmp/abc/hehe
ll /tmp/abc
-rw-rw-r--. 1 dmtsai dmtsai 0 Jul 9 19:16 hehe
看到了吧?如果 /tmp/abc 不存在時,touch 就不會被執行,若 /tmp/abc 存在的話,那麼 touch 就會開始執行囉! 很不錯用吧!不過,我們還得手動自行建立目錄,傷腦筋~能不能自動判斷,如果沒有該目錄就給予建立呢?
指令回傳值
範例二:測試 /tmp/abc 是否存在,若不存在則予以建立,若存在就不作任何事情
rm -r /tmp/abc <==先刪除此目錄以方便測試
ls /tmp/abc || mkdir /tmp/abc
ls: cannot access /tmp/abc: No such file or directory <==真的不存在喔!
ll -d /tmp/abc
drwxrwxr-x. 2 dmtsai dmtsai 6 Jul 9 19:17 /tmp/abc <==結果出現了!有進行 mkdir
如果你一再重複『 ls /tmp/abc || mkdir /tmp/abc 』畫面也不會出現重複 mkdir 的錯誤!這是因為 /tmp/abc 已經存在, 所以後續的 mkdir 就不會進行!這樣理解否?好了,讓我們再次的討論一下,如果我想要建立 /tmp/abc/hehe 這個檔案, 但我並不知道 /tmp/abc 是否存在,那該如何是好?試看看:
指令回傳值
範例三:我不清楚 /tmp/abc 是否存在,但就是要建立 /tmp/abc/hehe 檔案
ls /tmp/abc || mkdir /tmp/abc && touch /tmp/abc/hehe
上面這個範例三總是會嘗試建立 /tmp/abc/hehe 的喔!不論 /tmp/abc 是否存在。那麼範例三應該如何解釋呢? 由於Linux 底下的指令都是由左往右執行的,所以範例三有幾種結果我們來分析一下:
- 若 /tmp/abc 不存在故回傳 $?≠0,則 (2)因為 || 遇到非為 0 的 $? 故開始 mkdir /tmp/abc,由於 mkdir /tmp/abc 會成功進行,所以回傳 $?=0 (3)因為 && 遇到 $?=0 故會執行 touch /tmp/abc/hehe,最終 hehe 就被建立了;
- 若 /tmp/abc 存在故回傳 $?=0,則 (2)因為 || 遇到 0 的 $? 不會進行,此時 $?=0 繼續向後傳,故 (3)因為 && 遇到 $?=0 就開始建立 /tmp/abc/hehe 了!最終 /tmp/abc/hehe 被建立起來。
上面這張圖顯示的兩股資料中,上方的線段為不存在 /tmp/abc 時所進行的指令行為,下方的線段則是存在 /tmp/abc 所在的指令行為。如上所述,下方線段由於存在 /tmp/abc 所以導致 $?=0 ,讓中間的 mkdir 就不執行了! 並將 $?=0 繼續往後傳給後續的 touch 去利用啦!瞭乎?在任何時刻你都可以拿上面這張圖作為示意! 讓我們來想想底下這個例題吧!
- 例題:以 ls 測試 /tmp/vbirding 是否存在,若存在顯示 "exist" ,若不存在,顯示 "not exist"
- 答:這又牽涉到邏輯判斷的問題,如果存在就顯示某個資料,若不存在就顯示其他資料,那我可以這樣做:ls /tmp/vbirding && echo "exist" || echo "not exist"意思是說,當 ls /tmp/vbirding 執行後,若正確,就執行 echo "exist" ,若有問題,就執行 echo "not exist" !那如果寫成如下的狀況會出現什麼?ls /tmp/vbirding || echo "not exist" && echo "exist"這其實是有問題的,為什麼呢?由圖 10.5.2 的流程介紹我們知道指令是一個一個往後執行, 因此在上面的例子當中,如果 /tmp/vbirding 不存在時,他會進行如下動作:
- 若 ls /tmp/vbirding 不存在,因此回傳一個非為 0 的數值;
- 接下來經過 || 的判斷,發現前一個指令回傳非為 0 的數值,因此,程式開始執行 echo "not exist" ,而 echo "not exist" 程式肯定可以執行成功,因此會回傳一個 0 值給後面的指令;
- 經過 && 的判斷,咦!是 0 啊!所以就開始執行 echo "exist" 。所以啊,嘿嘿!第二個例子裡面竟然會同時出現 not exist 與 exist 呢!真神奇~
經過這個例題的練習,你應該會瞭解,由於指令是一個接著一個去執行的,因此,如果真要使用判斷, 那麼這個 && 與 || 的順序就不能搞錯。一般來說,假設判斷式有三個,也就是:
- command1 && command2 || command3
管線命令 pipe
就如同前面所說的, bash 命令執行的時候有輸出的資料會出現! 那麼如果這群資料必需要經過幾道手續之後才能得到我們所想要的 格式,應該如何來設定? 這就牽涉到管線命令的問題了 (pipe) ,管線命令使用的是『 | 』這個界定符號! 另外,管線命令與『連續下達命令』是不一樣的呦! 這點底下我們會再說明。底下我們先舉一個例子來說明一下簡單的管線命令。
假設我們想要知道 /etc/ 底下有多少檔案,那麼可以利用 ls /etc 來查閱,不過, 因為 /etc 底下的檔案太多,導致一口氣就將螢幕塞滿了~不知道前面輸出的內容是啥?此時,我們可以透過 less 指令的協助,利用:
ls -al /etc | less
如此一來,使用 ls 指令輸出後的內容,就能夠被 less 讀取,並且利用 less 的功能,我們就能夠前後翻動相關的資訊了!很方便是吧?我們就來瞭解一下這個管線命令『 | 』的用途吧! 其實這個管線命令『 | 』僅能處理經由前面一個指令傳來的正確資訊,也就是 standard output 的資訊,對於 stdandard error 並沒有直接處理的能力。
在每個管線後面接的第一個資料必定是『指令』喔!而且這個指令必須要能夠接受 standard input 的資料才行,這樣的指令才可以是為『管線命令』,例如 less, more, head, tail 等都是可以接受 standard input 的管線命令啦。至於例如 ls, cp, mv 等就不是管線命令了!因為 ls, cp, mv 並不會接受來自 stdin 的資料。 也就是說,管線命令主要有兩個比較需要注意的地方:
- 管線命令僅會處理 standard output,對於 standard error output 會予以忽略
- 管線命令必須要能夠接受來自前一個指令的資料成為 standard input 繼續處理才行。
- 2>&1 讓 standard error 可以被管線命令所使用
cut 同一行裡面的資料進行分解
cut
cut -d'分隔字元' -f fields <==用於有特定分隔字元
cut -c 字元區間 <==用於排列整齊的訊息
選項與參數:
-d :後面接分隔字元。與 -f 一起使用;
-f :依據 -d 的分隔字元將一段訊息分割成為數段,用 -f 取出第幾段的意思;
-c :以字元 (characters) 的單位取出固定字元區間;範例一:將 PATH 變數取出,我要找出第五個路徑。
echo ${PATH}
/usr/local/bin:/usr/bin:/usr/local/sbin:/usr/sbin:/home/dmtsai/.local/bin:/home/dmtsai/bin
# 1 | 2 | 3 | 4 | 5 | 6 |
echo ${PATH} | cut -d ':' -f 5# 如同上面的數字顯示,我們是以『 : 』作為分隔,因此會出現 /home/dmtsai/.local/bin
# 那麼如果想要列出第 3 與第 5 呢?,就是這樣:
echo ${PATH} | cut -d ':' -f 3,5範例二:將 export 輸出的訊息,取得第 12 字元以後的所有字串
export
declare -x HISTCONTROL="ignoredups"
declare -x HISTSIZE="1000"
declare -x HOME="/home/dmtsai"
declare -x HOSTNAME="study.centos.vbird"
.....(其他省略).....
# 注意看,每個資料都是排列整齊的輸出!如果我們不想要『 declare -x 』時,就得這麼做:
export | cut -c 12-
HISTCONTROL="ignoredups"
HISTSIZE="1000"
HOME="/home/dmtsai"
HOSTNAME="study.centos.vbird"
.....(其他省略).....
# 知道怎麼回事了吧?用 -c 可以處理比較具有格式的輸出資料!
# 我們還可以指定某個範圍的值,例如第 12-20 的字元,就是 cut -c 12-20 等等!範例三:用 last 將顯示的登入者的資訊中,僅留下使用者大名
last
root pts/1 192.168.201.101 Sat Feb 7 12:35 still logged in
root pts/1 192.168.201.101 Fri Feb 6 12:13 - 18:46 (06:33)
root pts/1 192.168.201.254 Thu Feb 5 22:37 - 23:53 (01:16)
# last 可以輸出『帳號/終端機/來源/日期時間』的資料,並且是排列整齊的
last | cut -d ' ' -f 1# 由輸出的結果我們可以發現第一個空白分隔的欄位代表帳號,所以使用如上指令:
# 但是因為 root pts/1 之間空格有好幾個,並非僅有一個,所以,如果要找出
# pts/1 其實不能以 cut -d ' ' -f 1,2 喔!輸出的結果會不是我們想要的。
grep 分析訊息, 取出所需要的資訊
grep
grep [-acinv] [--color=auto] '搜尋字串' filename
選項與參數:
-a :將 binary 檔案以 text 檔案的方式搜尋資料
-c :計算找到 '搜尋字串' 的次數
-i :忽略大小寫的不同,所以大小寫視為相同
-n :順便輸出行號
-v :反向選擇,亦即顯示出沒有 '搜尋字串' 內容的那一行!
--color=auto :可以將找到的關鍵字部分加上顏色的顯示喔!範例一:將 last 當中,有出現 root 的那一行就取出來;
last | grep 'root'範例二:與範例一相反,只要沒有 root 的就取出!
last | grep -v 'root'範例三:在 last 的輸出訊息中,只要有 root 就取出,並且僅取第一欄
last | grep 'root' |cut -d ' ' -f1# 在取出 root 之後,利用上個指令 cut 的處理,就能夠僅取得第一欄囉!範例四:取出 /etc/man_db.conf 內含 MANPATH 的那幾行
grep --color=auto 'MANPATH' /etc/man_db.conf....(前面省略)....MANPATH_MAP /usr/games /usr/share/man
MANPATH_MAP /opt/bin /opt/man
MANPATH_MAP /opt/sbin /opt/man
# 神奇的是,如果加上 --color=auto 的選項,找到的關鍵字部分會用特殊顏色顯示喔!
sort 排序
sort
sort [-fbMnrtuk] [file or stdin]
選項與參數:
-f :忽略大小寫的差異,例如 A 與 a 視為編碼相同;
-b :忽略最前面的空白字元部分;
-M :以月份的名字來排序,例如 JAN, DEC 等等的排序方法;
-n :使用『純數字』進行排序(預設是以文字型態來排序的);
-r :反向排序;
-u :就是 uniq ,相同的資料中,僅出現一行代表;
-t :分隔符號,預設是用 [tab] 鍵來分隔;
-k :以那個區間 (field) 來進行排序的意思範例一:個人帳號都記錄在 /etc/passwd 下,請將帳號進行排序。
cat /etc/passwd | sortabrt:x:173:173::/etc/abrt:/sbin/nologin
adm:x:3:4:adm:/var/adm:/sbin/nologin
alex:x:1001:1002::/home/alex:/bin/bash
# 鳥哥省略很多的輸出~由上面的資料看起來, sort 是預設『以第一個』資料來排序,
# 而且預設是以『文字』型態來排序的喔!所以由 a 開始排到最後囉!範例二:/etc/passwd 內容是以 : 來分隔的,我想以第三欄含後面資料來排序,該如何?
cat /etc/passwd | sort -t ':' -k 3
root:x:0:0:root:/root:/bin/bash
dmtsai:x:1000:1000:dmtsai:/home/dmtsai:/bin/bash
alex:x:1001:1002::/home/alex:/bin/bash
arod:x:1002:1003::/home/arod:/bin/bash
# 看到特殊字體的輸出部分了吧?怎麼會這樣排列啊?呵呵!沒錯啦~若單純以第三欄位來處理則是:
cat /etc/passwd | sort -t ':' -k 3,3
# 如果是以文字型態來排序的話,原本就會是這樣,想要使用數字排序:
# cat /etc/passwd | sort -t ':' -k 3,3 -n
# 這樣才行啊!用那個 -n 來告知 sort 以數字來排序啊!
範例三:利用 last ,將輸出的資料僅取帳號,並加以排序
last | cut -d ' ' -f1 | sort
uniq 將重複的行刪除掉只顯示一個
uniq
uniq [-ic]
選項與參數:
-i :忽略大小寫字元的不同;
-c :進行計數範例一:使用 last 將帳號列出,僅取出帳號欄,進行排序後僅取出一位;
last | cut -d ' ' -f1 | sort | uniq範例二:承上題,如果我還想要知道每個人的登入總次數呢?
last | cut -d ' ' -f1 | sort | uniq -c
1
6 (unknown
47 dmtsai
4 reboot
7 root
1 wtmp
# 從上面的結果可以發現 reboot 有 4 次, root 登入則有 7 次!大部分是以 dmtsai 來操作!
# wtmp 與第一行的空白都是 last 的預設字元,那兩個可以忽略的!
wc 計算行數
wc
wc [-lwm]
選項與參數:
-l :僅列出行;
-w :僅列出多少字(英文單字);
-m :多少字元;範例一:那個 /etc/man_db.conf 裡面到底有多少相關字、行、字元數?
cat /etc/man_db.conf | wc
131 723 5171
# 輸出的三個數字中,分別代表: 『行、字數、字元數』範例二:我知道使用 last 可以輸出登入者,但是 last 最後兩行並非帳號內容,那麼請問,
我該如何以一行指令串取得登入系統的總人次?
last | grep [a-zA-Z] | grep -v 'wtmp' | grep -v 'reboot' | \\
>grep -v 'unknown' |wc -l# 由於 last 會輸出空白行, wtmp, unknown, reboot 等無關帳號登入的資訊,因此,我 利用
# grep 取出非空白行,以及去除上述關鍵字那幾行,再計算行數,就能夠瞭解囉!
tee 雙向重導向
想個簡單的東西,我們由前一節知道 > 會將資料流整個傳送給檔案或裝置,因此我們除非去讀取該檔案或裝置, 否則就無法繼續利用這個資料流。萬一我想要將這個資料流的處理過程中將某段訊息存下來,應該怎麼做? 利用 tee 就可以囉~我們可以這樣簡單的看一下:
tee 會同時將資料流分送到檔案去與螢幕 (screen);而輸出到螢幕的,其實就是 stdout ,那就可以讓下個指令繼續處理喔!
tee
tee [-a] file
選項與參數:
-a :以累加 (append) 的方式,將資料加入 file 當中!
last | tee last.list | cut -d " " -f1# 這個範例可以讓我們將 last 的輸出存一份到 last.list 檔案中;
ls -l /home | tee ~/homefile | more# 這個範例則是將 ls 的資料存一份到 ~/homefile ,同時螢幕也有輸出訊息!
ls -l / | tee -a ~/homefile | more# 要注意! tee 後接的檔案會被覆蓋,若加上 -a 這個選項則能將訊息累加。
tee 可以讓 standard output 轉存一份到檔案內並將同樣的資料繼續送到螢幕去處理! 這樣除了可以讓我們同時分析一份資料並記錄下來之外,還可以作為處理一份資料的中間暫存檔記錄之用! tee 這傢伙在很多選擇/填充的認證考試中很容易考呢!
tr 刪除一段訊息當中的文字,或者是進行文字訊息的替換
tr
tr [-ds] SET1 ...
選項與參數:
-d :刪除訊息當中的 SET1 這個字串;
-s :取代掉重複的字元!範例一:將 last 輸出的訊息中,所有的小寫變成大寫字元:
last | tr '[a-z]' '[A-Z]'# 事實上,沒有加上單引號也是可以執行的,如:『 last | tr [a-z] [A-Z] 』範例二:將 /etc/passwd 輸出的訊息中,將冒號 (:) 刪除
cat /etc/passwd | tr -d ':'範例三:將 /etc/passwd 轉存成 dos 斷行到 /root/passwd 中,再將 ^M 符號刪除
cp /etc/passwd ~/passwd && unix2dos ~/passwd
file /etc/passwd ~/passwd
/etc/passwd: ASCII text
/home/dmtsai/passwd: ASCII text, with CRLF line terminators <==就是 DOS 斷行
cat ~/passwd | tr -d '\\r' > ~/passwd.linux# 那個 \\r 指的是 DOS 的斷行字元,關於更多的字符,請參考 man tr
ll /etc/passwd ~/passwd*
-rw-r--r--. 1 root root 2092 Jun 17 00:20 /etc/passwd
-rw-r--r--. 1 dmtsai dmtsai 2133 Jul 9 22:13 /home/dmtsai/passwd
-rw-rw-r--. 1 dmtsai dmtsai 2092 Jul 9 22:13 /home/dmtsai/passwd.linux
# 處理過後,發現檔案大小與原本的 /etc/passwd 就一致了!
join 兩個檔案中,有 相同資料的那一行,將他加在一起
join
join [-ti12] file1 file2
選項與參數:
-t :join 預設以空白字元分隔資料,並且比對『第一個欄位』的資料,
如果兩個檔案相同,則將兩筆資料聯成一行,且第一個欄位放在第一個!
-i :忽略大小寫的差異;
-1 :這個是數字的 1 ,代表『第一個檔案要用那個欄位來分析』的意思;
-2 :代表『第二個檔案要用那個欄位來分析』的意思。範例一:用 root 的身份,將 /etc/passwd 與 /etc/shadow 相關資料整合成一欄
[root@study ~]#head -n 3 /etc/passwd /etc/shadow
==> /etc/passwd <==
root:x:0:0:root:/root:/bin/bash
bin:x:1:1:bin:/bin:/sbin/nologin
daemon:x:2:2:daemon:/sbin:/sbin/nologin
==> /etc/shadow <==
root:$6$wtbCCce/PxMeE5wm$KE2IfSJr...:16559:0:99999:7:::
bin:*:16372:0:99999:7:::
daemon:*:16372:0:99999:7:::
# 由輸出的資料可以發現這兩個檔案的最左邊欄位都是相同帳號!且以 : 分隔
join -t ':' /etc/passwd /etc/shadow | head -n 3root:x:0:0:root:/root:/bin/bash:$6$wtbCCce/PxMeE5wm$KE2IfSJr...:16559:0:99999:7:::
bin:x:1:1:bin:/bin:/sbin/nologin:*:16372:0:99999:7:::
daemon:x:2:2:daemon:/sbin:/sbin/nologin:*:16372:0:99999:7:::
# 透過上面這個動作,我們可以將兩個檔案第一欄位相同者整合成一列!
# 第二個檔案的相同欄位並不會顯示(因為已經在最左邊的欄位出現了啊!)範例二:我們知道 /etc/passwd 第四個欄位是 GID ,那個 GID 記錄在
/etc/group 當中的第三個欄位,請問如何將兩個檔案整合?
head -n 3 /etc/passwd /etc/group
==> /etc/passwd <==
root:x:0:0:root:/root:/bin/bash
bin:x:1:1:bin:/bin:/sbin/nologin
daemon:x:2:2:daemon:/sbin:/sbin/nologin
==> /etc/group <==
root:x:0:
bin:x:1:
daemon:x:2:
# 從上面可以看到,確實有相同的部分喔!趕緊來整合一下!
join -t ':' -1 4 /etc/passwd -2 3 /etc/group | head -n 30:root:x:0:root:/root:/bin/bash:root:x:
1:bin:x:1:bin:/bin:/sbin/nologin:bin:x:
2:daemon:x:2:daemon:/sbin:/sbin/nologin:daemon:x:
# 同樣的,相同的欄位部分被移動到最前面了!所以第二個檔案的內容就沒再顯示。
# 請讀者們配合上述顯示兩個檔案的實際內容來比對!
這個 join 在處理兩個相關的資料檔案時,就真的是很有幫助的啦! 例如上面的案例當中,我的 /etc/passwd, /etc/shadow, /etc/group 都是有相關性的, 其中 /etc/passwd, /etc/shadow 以帳號為相關性,至於 /etc/passwd, /etc/group 則以所謂的 GID (帳號的數字定義) 來作為他的相關性。根據這個相關性, 我們可以將有關係的資料放置在一起!這在處理資料可是相當有幫助的!
paste 將兩行貼在一起,且中間以 [tab] 鍵隔開
paste
paste [-d] file1 file2
選項與參數:
-d :後面可以接分隔字元。預設是以 [tab] 來分隔的!
- :如果 file 部分寫成 - ,表示來自 standard input 的資料的意思。範例一:用 root 身份,將 /etc/passwd 與 /etc/shadow 同一行貼在一起
paste /etc/passwd /etc/shadowroot:x:0:0:root:/root:/bin/bash root:$6$wtbCCce/PxMeE5wm$KE2IfSJr...:16559:0:99999:7:::
bin:x:1:1:bin:/bin:/sbin/nologin bin:*:16372:0:99999:7:::
daemon:x:2:2:daemon:/sbin:/sbin/nologin daemon:*:16372:0:99999:7:::
# 注意喔!同一行中間是以 [tab] 按鍵隔開的!範例二:先 將 /etc/group 讀出(用 cat),然後與範例一貼上一起!且僅取出前三行
cat /etc/group|paste /etc/passwd /etc/shadow -|head -n 3# 這個例子的重點在那個 - 的使用!那玩意兒常常代表 stdin 喔!
split 分割命令
split
split [-bl] file PREFIX
選項與參數:
-b :後面可接欲分割成的檔案大小,可加單位,例如 b, k, m 等;
-l :以行數來進行分割。
PREFIX :代表前置字元的意思,可作為分割檔案的前導文字。範例一:我的 /etc/services 有六百多K,若想要分成 300K 一個檔案時?
cd /tmp; split -b 300k /etc/services services
ll -k services*
-rw-rw-r--. 1 dmtsai dmtsai 307200 Jul 9 22:52 servicesaa
-rw-rw-r--. 1 dmtsai dmtsai 307200 Jul 9 22:52 servicesab
-rw-rw-r--. 1 dmtsai dmtsai 55893 Jul 9 22:52 servicesac
# 那個檔名可以隨意取的啦!我們只要寫上前導文字,小檔案就會以
# xxxaa, xxxab, xxxac 等方式來建立小檔案的!範例二:如何將上面的三 個小檔案合成一個檔案,檔名為 servicesback
cat services* >> servicesback# 很簡單吧?就用資料流重導向就好啦!簡單!
範例三:使用 ls -al / 輸出的資訊中,每十行記錄成一個檔案
ls -al / | split -l 10 - lsroot
wc -l lsroot*
10 lsrootaa
10 lsrootab
4 lsrootac
24 total
# 重點在那個 - 啦!一般來說,如果需要 stdout/stdin 時,但偏偏又沒有檔案,
# 有的只是 - 時,那麼那個 - 就會被當成 stdin 或 stdout ~
xargs 參數代換
產生某個指令的參數。 xargs 可以讀入 stdin 的資料,並且以空白字元或斷行字元作為分辨,將 stdin 的資料分隔成為 arguments 。
xargs
xargs [-0epn] command
選項與參數:
-0 :如果輸入的 stdin 含有特殊字元,例如 `, \\, 空白鍵等等字元時,這個 -0 參數
可以將他還原成一般字元。這個參數可以用於特殊狀態喔!
-e :這個是 EOF (end of file) 的意思。後面可以接一個字串,當 xargs 分析到這個字串時,
就會停止繼續工作!
-p :在執行每個指令的 argument 時,都會詢問使用者的意思;
-n :後面接次數,每次 command 指令執行時,要使用幾個參數的意思。
當 xargs 後面沒有接任何的指令時,預設是以 echo 來進行輸出喔!範例一:將 /etc/passwd 內的第一欄取出,僅取三行,使用 id 這個指令將每個帳號內容秀出來
id root
uid=0(root) gid=0(root) groups=0(root) # 這個 id 指令可以查詢使用者的 UID/GID 等資訊
id $(cut -d ':' -f 1 /etc/passwd | head -n 3)# 雖然使用 $(cmd) 可以預先取得參數,但可惜的是, id 這個指令『僅』能接受一個參數而已!
# 所以上述的這個指令執行會出現錯誤!根本不會顯示用戶的 ID 啊!
cut -d ':' -f 1 /etc/passwd | head -n 3 | id
uid=1000(dmtsai) gid=1000(dmtsai) groups=1000(dmtsai),10(wheel) # 我不是要查自己啊!
# 因為 id 並不是管線命令,因此在上面這個指令執行後,前面的東西通通不見!只會執行 id!
cut -d ':' -f 1 /etc/passwd | head -n 3 | xargs id# 依舊會出現錯誤!這是因為 xargs 一口氣將全部的資料通通丟給 id 處理~但 id 就接受 1 個啊最多!
cut -d ':' -f 1 /etc/passwd | head -n 3 | xargs -n 1 id
uid=0(root) gid=0(root) groups=0(root)
uid=1(bin) gid=1(bin) groups=1(bin)
uid=2(daemon) gid=2(daemon) groups=2(daemon)
# 透過 -n 來處理,一次給予一個參數,因此上述的結果就 OK 正常的顯示囉!範例二:同上,但是每次執行 id 時,都要詢問使用者是否動作?
cut -d ':' -f 1 /etc/passwd | head -n 3 | xargs -p -n 1 id
id root ?...y
uid=0(root) gid=0(root) groups=0(root)
id bin ?...y.....(底下省略).....
# 呵呵!這個 -p 的選項可以讓使用者的使用過程中,被詢問到每個指令是否執行!範例三:將所有的 /etc/passwd 內的帳號都以 id 查閱,但查到 sync 就結束指令串
cut -d ':' -f 1 /etc/passwd | xargs -e'sync' -n 1 id# 仔細與上面的案例做比較。也同時注意,那個 -e'sync' 是連在一起的,中間沒有空白鍵。
# 上個例子當中,第六個參數是 sync 啊,那麼我們下達 -e'sync' 後,則分析到 sync 這個字串時,
# 後面的其他 stdin 的內容就會被 xargs 捨棄掉了!